# MethodDispatcher.java

    package org.codefilarete.tool.reflect;

    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.Map;
    import java.util.Set;
    import java.util.function.Supplier;

    import org.codefilarete.tool.InvocationHandlerSupport;
    import org.codefilarete.tool.Reflections;
    import org.codefilarete.tool.StringAppender;
    import org.codefilarete.tool.Strings;
    import org.codefilarete.tool.collection.Iterables;
    import org.codefilarete.tool.function.Functions;

    /**
     * A class aimed at creating proxy that will redirect some interface methods to some concrete implementation, fallback non-redirecting method
     * to a concrete instance.
     * This allows to "extend" or decorate an instance (the fallback one) with some additional feature.
     *
     * @author Guillaume Mary
     */
    public class MethodDispatcher {

    	/**
    	 * Methods (and its config) to be invoked according to effective method call.
    	 * These are stored per a light signature to take polymorphism and multiple inheritance into account.
    	 * Indeed, the light signature doesn't contain declaring class nor return type : only method name and arguments types.
    	 */
    	protected final Map<String /* simple method signature */, Interceptor> interceptors = new HashMap<>();

    	/** Final target when no interceptor handled method, not null after {@link #build(Class)} invocation */
    	private Supplier<?> fallback;

    	public Map<String, Interceptor> getInterceptors() {
    		return interceptors;
    	}

    	/**
    	 * Redirects all interfazz methods to extensionDelegate
    	 * @param interfazz an interface, must be extended by the one that will be given by {@link #build(Class)} (should be a super type of it)
    	 * @param extensionDelegate an instance implementing X so methods of X can be redirected to it
    	 * @param <X> a interface type
    	 * @return this
    	 */
    	public <X> MethodDispatcher redirect(Class<X> interfazz, X extensionDelegate) {
    		return redirect(interfazz, extensionDelegate, false);
    	}

    	/**
    	 * Same as {@link #redirect(Class, Object)} but proxy will be returned by all methods invocation : result of them will be ignored.
    	 * Useful when return types of extensionDelegate methods don't match those of {@link #build(Class)} (kind of rare case).
    	 *
    	 * @param interfazz an interface, must be extended by the one that will be given to {@link #build(Class)} (should be a super type of it)
    	 * @param extensionDelegate an instance implementing X so methods of X can be redirected to it
    	 * @param <X> an interface type
    	 * @return this
    	 */
    	public <X> MethodDispatcher redirect(Class<X> interfazz, X extensionDelegate, boolean returnProxy) {
    		for (Method method : interfazz.getMethods()) {
    			addInterceptor(method, extensionDelegate, returnProxy);
    		}
    		return this;
    	}

    	/**
    	 * Same as {@link #redirect(Class, Object)} but given object will be returned by all method invocation.
    	 * Useful when creating a fluent API where given interfazz methods should be invoked once, then methods return type is not herself but another
    	 * one, the one that given returningMethodsTarget must implement (kind of rare case).
    	 *
    	 * @param interfazz an interface, must be extended by the one that will be given to {@link #build(Class)} (should be a super type of it)
    	 * @param extensionDelegate an instance implementing X so methods of X can be redirected to it
    	 * @param returningMethodsTarget an instance that implements all interfazz' methods return types
    	 * @param <X> an interface type
    	 * @return this
    	 */
    	public <X> MethodDispatcher redirect(Class<X> interfazz, X extensionDelegate, Object returningMethodsTarget) {
    		for (Method method : interfazz.getMethods()) {
    			addInterceptor(method, extensionDelegate, returningMethodsTarget);
    		}
    		return this;
    	}

    	protected  <X> void addInterceptor(Method method, X extensionDelegate, boolean returnProxy) {
    		interceptors.put(giveSignature(method), new Interceptor(method, extensionDelegate, returnProxy));
    	}

    	protected  <X> void addInterceptor(Method method, X extensionDelegate, Object returningMethodsTarget) {
    		interceptors.put(giveSignature(method), new Interceptor(method, extensionDelegate, returningMethodsTarget));
    	}

    	/**
    	 * Gives a very ligth signature of a method (no return type, no modifiers) : only name and arguments
    	 *
    	 * @param method a method to get a signature
    	 * @return a lightweight version of the signature of the given method
    	 */
    	protected String giveSignature(Method method) {
    		StringAppender signature = new StringAppender();
    		signature.cat(method.getName());
    		signature.ccat(method.getParameterTypes(), ",");
    		return signature.toString();
    	}

    	public <X> X build(Class<X> interfazz) {
    		assertInterceptingMethodsAreFromInterfaces();
    		assertClassImplementsInterceptingInterface(interfazz);

    		// Which ClassLoader ? Thread, fallback, this ?
    		// we don't use the Thread one because it can live forever so it might lead to memory leak
    		// we don't use fallback because it can be null
    		ClassLoader classLoader = getClass().getClassLoader();
    		Set<Class<?>> targetInterfaces = Iterables.collect(interceptors.values(), Functions.chain(Interceptor::getMethod, Method::getDeclaringClass), HashSet::new);
    		// we must add the X interface to the list that will be proxied, else we'll get a "com.sun.proxy.$Proxy4 cannot be cast to X"
    		targetInterfaces.add(interfazz);
    		// building invocationHandler : we create a holder for the proxy because it must be referenced in some cases
    		Object[] proxyHolder = new Object[1];
    		InvocationHandler dispatcher = new InvocationHandlerSupport((input, method, args) -> {
    			// looking for method to be really invoked
                String key = giveSignature(method);
                Interceptor interceptor = interceptors.get(key);
    			Object targetInstance;
    			boolean returnProxy = false;
    			Object returningMethodsTarget = null;
    			if (interceptor != null) {
    				// NB: the method finally invoked may not be the same as the one invoked
    				// in polymorphism and multiple inheritance cases
    				method = interceptor.getMethod();
    				targetInstance = interceptor.getMethodTarget();
    				returnProxy = interceptor.isReturnProxy();
    				returningMethodsTarget = interceptor.getReturningMethodsTarget();
    			} else {
    				if (fallback == null && !Reflections.isStatic(method)) {
    					throw new NullPointerException("No fallback instance was declared, therefore calling " + Reflections.toString(method)
    							+ " would throw NullPointerException: try to set one or redirect given method to a compatible instance");
    				}
    				targetInstance = fallback.get();
    			}
    			Object result = invoke(targetInstance, method, args);

    			// Handling return cases
    			if (returnProxy) {
    				result = proxyHolder[0];
    			} else if (returningMethodsTarget != null) {
    				result = returningMethodsTarget;
    			}
    			return result;
    		}) {
    			/** We handle toString() to have some message indicating who we are to avoid "ghost" behavior and hard debug */
    			@Override
    			public String toString() {
    				return "Dispatcher to " + fallback.get().toString();
    			}
    		};
    		proxyHolder[0] = Proxy.newProxyInstance(classLoader, targetInterfaces.toArray(new Class[0]), dispatcher);
    		return (X) proxyHolder[0];
    	}

    	private <X> void assertClassImplementsInterceptingInterface(Class<X> interfazz) {
    		interceptors.values().forEach(m -> {
    			if (!m.getMethod().getDeclaringClass().isAssignableFrom(interfazz)) {
    				throw new IllegalArgumentException(
    						Reflections.toString(interfazz) + " doesn't implement " + Reflections.toString(m.getMethod().getDeclaringClass()));
    			}
    		});
    	}

    	private void assertInterceptingMethodsAreFromInterfaces() {
    		interceptors.values().forEach(m -> {
    			if (!m.getMethod().getDeclaringClass().isInterface()) {
    				throw new UnsupportedOperationException("Cannot intercept concrete method : " + Reflections.toString(m.getMethod()));
    			}
    		});
    	}

    	/**
    	 * Indicates on which instance unregistered methods will be invoked
    	 * @param fallback the instance on which unregistered methods will be invoked
    	 * @return this
    	 */
    	public MethodDispatcher fallbackOn(Object fallback) {
    		return fallbackOn(() -> fallback);
    	}

    	public MethodDispatcher fallbackOn(Supplier<?> fallback) {
    		this.fallback = fallback;
    		return this;
    	}

    	/**
    	 * Invokes a method on a target
    	 * @param target instance target of the method
    	 * @param method the method to be called
    	 * @param args method arguments
    	 * @return the result of invocation of the given method and arguments on given instance
    	 * @throws Throwable the one thrown by method invocation or one wrapping it for better understanding
    	 */
    	protected Object invoke(Object target, Method method, Object[] args) throws Throwable {
    		try {
    			return method.invoke(target, args);
    		} catch (InvocationTargetException e) {
    			Throwable cause = e.getCause();
    			if (cause instanceof java.lang.ClassCastException) {
    				throw new WrongTypeReturnedException((java.lang.ClassCastException) cause, method);
    			}
    			// we rethrow the main exception so caller will not be polluted by InvocationTargetException
    			throw cause;
    		} catch (IllegalArgumentException e) {
    			// See ExceptionConverter for some code correlation
    			IllegalArgumentException result = e;
    			if ("object is not an instance of declaring class".equals(e.getMessage())) {
    				Class<?> declaringClass = method.getDeclaringClass();
    				String message = "Wrong given instance while invoking " + Reflections.toString(method);
    				message += ": expected " + declaringClass.getName() + " but " + target + " was given";
    				result = new IllegalArgumentException(message, e);
    			}
    			throw result;
    		}
    	}

    	protected static class Interceptor {

    		private final Method method;
    		private final Object methodTarget;
    		private final boolean returnProxy;
    		private final Object returningMethodsTarget;

    		public Interceptor(Method method, Object methodTarget, boolean returnProxy) {
    			this.method = method;
    			this.methodTarget = methodTarget;
    			this.returnProxy = returnProxy;
    			this.returningMethodsTarget = null;
    		}

    		public Interceptor(Method method, Object methodTarget, Object returningMethodsTarget) {
    			this.method = method;
    			this.methodTarget = methodTarget;
    			this.returnProxy = false;
    			this.returningMethodsTarget = returningMethodsTarget;
    		}

    		public Method getMethod() {
    			return method;
    		}

    		public Object getMethodTarget() {
    			return methodTarget;
    		}

    		public boolean isReturnProxy() {
    			return returnProxy;
    		}

    		public Object getReturningMethodsTarget() {
    			return returningMethodsTarget;
    		}
    	}

    	public static class WrongTypeReturnedException extends RuntimeException {

    		private final Class expected;
    		private final Class given;
    		private final Method method;

    		public WrongTypeReturnedException(java.lang.ClassCastException classCastException, Method method) {
    			this(Reflections.forName(Strings.tail(classCastException.getMessage(), " cannot be cast to ").toString()),
    					Reflections.forName(Strings.head(classCastException.getMessage(), " cannot be cast to ").toString()),
    					method,
    					classCastException);
    		}

    		public WrongTypeReturnedException(Class expected, Class given, Method method, java.lang.ClassCastException cause) {
    			super(cause);
    			this.expected = expected;
    			this.given = given;
    			this.method = method;
    		}

    		@Override
    		public String getMessage() {
    			return "Code handling " + Reflections.toString(method) + " is expected to return a " + Reflections.toString(expected) + " but returned a " + Reflections.toString(given);
    		}
    	}
    }